<script lang="ts">
import { Component, Vue, Model, Prop, Watch } from 'vue-property-decorator'
import { Form } from 'element-ui'
import { CreateElement, VNode } from 'vue'
import RenderFormItem from './RenderFormItem.vue'
import {
  FormModel,
  FormField,
  FormFieldAttrs,
  FormFieldEvents
} from './normal-type'

@Component({
  name: 'RenderForm',
  components: { RenderFormItem }
})
export default class extends Vue {
  @Model('model-change', { type: Object, required: false, default: () => ({}) })
  private model!: FormModel

  // form item list
  @Prop({ type: Array, required: false, default: () => [] })
  private fields!: Array<FormField>
  // whether to display the operation buttons
  @Prop({ type: Boolean, required: false, default: true })
  private showFooter!: boolean
  // loading state
  @Prop({ type: Boolean, required: false, default: false })
  private loading!: boolean

  private currentModel: FormModel = { ...this.model }

  @Watch('model', { deep: true })
  private onModelChange (val: object, oldVal: object): void {
    if (JSON.stringify(val) !== JSON.stringify(this.currentModel)) {
      this.currentModel = { ...val }
    }
  }

  private handleUnlock (): void {
    this.$emit('unlock')
  }
  private handleCancel (): void {
    this.$emit('cancel')
    this.$nextTick(() => {
      ;(this.$refs.form as Form).clearValidate()
    })
  }
  private handleReset (): void {
    this.$emit('reset')
    this.$nextTick(() => {
      ;(this.$refs.form as Form).clearValidate()
    })
  }
  private handleSubmit (): void {
    ;(this.$refs.form as Form).validate((valid: boolean, obj: object) => {
      if (valid) {
        this.$emit('submit', { ...this.currentModel })
      } else {
        this.showNotify(obj)
      }
    })
  }

  private showNotify (obj: object): void {
    const h = this.$createElement

    let arr = this.fields.reduce((acc: Array<string>, _: FormField) => {
      if (!_.hide && obj.hasOwnProperty(_.prop)) {
        acc.push(_.label!)
      }
      return acc
    }, [])

    let message = h('div', [
      h('p', 'check the following'),
      h('p', arr.join(', '))
    ])

    this.$notify({
      title: 'please fill in the form',
      message,
      type: 'error',
      showClose: false
    })
  }

  private handleRangeValueChange (
    val: Array<string | number | Date> | null,
    keys: Array<string>
  ): void {
    if (keys && keys.length === 2) {
      let [start, end] = keys
      if (val && val.length === 2) {
        this.currentModel[start] = val[0]
        this.currentModel[end] = val[1]
      } else {
        delete this.currentModel[start]
        delete this.currentModel[end]
      }

      this.$emit('model-change', { ...this.currentModel })
    }
  }

  render (h: CreateElement) {
    let formItems = this.fields.reduce((acc: Array<any>, _: FormField) => {
      if (!_.hide) {
        let attrs = { ..._ }
        delete attrs.hide
        delete attrs.attrs
        delete attrs.slot
        delete attrs.events

        let props = {
          value: this.currentModel[_.prop],
          type: _.type,
          innerProps: { ..._.attrs, innerSlot: _.slot }
        }

        let on: FormFieldEvents = {
          ..._.events,
          input: (v: any) => {
            this.$emit('model-change', { ...this.currentModel, [_.prop]: v })
          }
        }

        /* implicit event */
        // range value auto separate
        if (
          ['date-picker', 'time-picker', 'input-number-ratio'].includes(
            _.type
          ) &&
          _.attrs &&
          _.attrs.rangeKeys &&
          _.attrs.rangeKeys.length === 2 &&
          this.currentModel[_.prop] instanceof Array
        ) {
          on.change = (v: Array<string | number | Date> | null) => {
            this.handleRangeValueChange(v, _.attrs!.rangeKeys)
            if (_.events && _.events.hasOwnProperty('change')) {
              _.events.change(v)
            }
          }
        }

        acc.push(h('RenderFormItem', { attrs, props, on, key: _.prop }))
      }
      return acc
    }, [])

    let slotKeys = Object.keys(this.$slots)
      .filter(_ => /^sort-\d+$/.test(_))
      .sort(
        (a, b) =>
          Number(a.replace(/^sort-/, '')) - Number(b.replace(/^sort-/, ''))
      )
    slotKeys.push('default', 'end')

    slotKeys.forEach(_ => {
      if (this.$slots.hasOwnProperty(_)) {
        if (/^sort-/.test(_)) {
          let idx = Number(_.replace(/^sort-/, ''))
          formItems.splice(idx, 0, this.$slots[_])
        }

        if (_ === 'default') {
          formItems.unshift(this.$slots.default)
        }
        if (_ === 'end') {
          formItems.push(this.$slots.end)
        }
      }
    })

    let form = [
      h(
        'el-form',
        {
          attrs: this.$attrs,
          props: {
            'label-width': '80px',
            'status-icon': true,
            ...this.$attrs,
            model: this.currentModel
          },
          directives: [
            { name: 'loading', value: this.loading, modifiers: { lock: true } }
          ],
          ref: 'form'
        },
        formItems
      )
    ]
    if (this.showFooter) {
      let buttons = [
        {
          show: !!this.$attrs.disabled,
          type: 'info',
          icon: 'el-icon-unlock',
          click: this.handleUnlock
        },
        {
          show: !this.$attrs.disabled,
          type: 'danger',
          icon: 'el-icon-close',
          click: this.handleCancel
        },
        {
          show: !this.$attrs.disabled,
          type: 'warning',
          icon: 'el-icon-refresh',
          click: this.handleReset
        },
        {
          show: !this.$attrs.disabled,
          type: 'primary',
          icon: 'el-icon-check',
          click: this.handleSubmit
        }
      ].reduce((acc: Array<VNode>, _) => {
        if (_.show) {
          let btn = h('el-button', {
            props: {
              type: _.type,
              circle: true,
              disabled: this.$attrs.disabled ? false : this.loading,
              icon: _.icon
            },
            on: { click: _.click }
          })
          acc.push(btn)
        }
        return acc
      }, [])
      let footer = h('div', { style: { textAlign: 'center' } }, buttons)
      form.push(footer)
    }

    return h('div', form)
  }
}
</script>
